package com.meshnetics.gb.stack.mac;

import com.meshnetics.gb.event.Event;
import com.meshnetics.gb.event.EventListener;
import com.meshnetics.gb.stack.core.InformationBase;
import com.meshnetics.gb.stack.core.event.*;
import com.meshnetics.gb.stack.core.event.system.FailureStackEvent;
import com.meshnetics.gb.stack.core.event.system.SystemCodes;
import com.meshnetics.gb.stack.core.layer.AbstractStackLayer;
import com.meshnetics.gb.stack.core.layer.SimpleIDGenerator;
import com.meshnetics.gb.stack.core.layer.StackLayer;
import com.meshnetics.gb.stack.mac.io.LoggerInputStream;
import com.meshnetics.gb.stack.mac.io.LoggerOutputStream;
import com.meshnetics.gb.stack.mac.io.UARTConnectionFactory;
import com.meshnetics.gb.stack.mac.primitives.MACCommand;
import com.meshnetics.gb.stack.mac.primitives.MACReplyAwaited;
import com.meshnetics.gb.stack.mac.primitives.MACReplyOnCommand;
import com.meshnetics.gb.stack.mac.util.SingleThreadExecutor;
import com.meshnetics.gb.util.ToStringUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.microedition.io.StreamConnection;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Stack layer - wrap of physical implementation of mac.
 *
 * @author AKovylin
 */
public class RadioMACStackLayer extends AbstractStackLayer {
    public static final String SYSTEM_PROPERTY_CONNECTION = "meshnetics.mac.connection";

    private static final Log log = LogFactory.getLog(RadioMACStackLayer.class);
    private static final int MAX_BUF_SIZE = 255;
    private static final int STATE_STOPPED = -1;
    private static final int STATE_TRANSITION = 0;
    private static final int STATE_STARTED = 1;

    private final EventDispatcher dispatcher = new EventDispatcher() {
        public void dispatch(StackEvent evt) {
            throw new UnsupportedOperationException("RadioMACStackLayer doesn't support dispatcher");
        }

        public void dispatch(StackRequest req, StackLayer layer) {
            throw new UnsupportedOperationException("RadioMACStackLayer doesn't support dispatcher");
        }

        public void dispatch(StackResponse resp, StackLayer layer) {
            throw new UnsupportedOperationException("RadioMACStackLayer doesn't support dispatcher");
        }
    };
    private final EventListener listener = new EventListener() {
        public void handle(Event evt) {
            if (evt instanceof StackEvent) {
                launch((StackEvent) evt);
            } else {
                throw new IllegalArgumentException("unsupported " + evt);
            }
        }
    };

    private StreamConnection connection = null;
    private OutputStream outputStream = null;
    private RadioMACFrameReader eventReader = null;
    private SingleThreadExecutor upperLayerNotifier;
    private final PrimitivesFactory primitivesFactory;
    private final ByteBuffer writeBuffer = ByteBuffer.allocate(MAX_BUF_SIZE).order(ByteOrder.LITTLE_ENDIAN);
    private final int[] state = new int[]{STATE_STOPPED};
    private final List activeCommands = new ArrayList();

    public RadioMACStackLayer(String name, InformationBase ib) throws NoSuchMethodException {
        super(name, ib, new SimpleIDGenerator());
        this.primitivesFactory = PrimitivesFactory.newMACPrimitivesFactory(this);
    }

    // --- simple interface methods

    public EventDispatcher getEventDispatcher() {
        return dispatcher;
    }

    public EventListener getEventListener() {
        return listener;
    }

    public void addLowerLayer(StackLayer layer) {
        throw new UnsupportedOperationException("lower layers are not supported");
    }

    public void removeLowerLayer(StackLayer layer) {
        throw new UnsupportedOperationException("lower layers are not supported");
    }

    public StackLayer[] getLowerLayers() {
        return new StackLayer[0];
    }

    // --- start/stop

    public void start() {
        synchronized (state) {
            if (state[0] != STATE_STOPPED) {
                throw new IllegalStateException("layer is not stopped to start");
            }
            state[0] = STATE_TRANSITION;
        }
        if (log.isInfoEnabled()) {
            log.info("starting...");
        }
        try {
            UARTConnectionFactory factory = new UARTConnectionFactory();
            String s = System.getProperty(SYSTEM_PROPERTY_CONNECTION);
            if (s == null) {
                throw new IllegalStateException(
                        "system property 'gateway.mac.connection' doesn't have connection URL");
            }
            if (log.isInfoEnabled()) {
                log.info("connecting to " + s);
            }
            connection = (StreamConnection) factory.createConnection(s, 0, false); // mode and timeControl not used atm
            if (log.isInfoEnabled()) {
                log.info("connection created");
            }
            int idx = s.indexOf('?');
            String port = idx == -1 ? s : s.substring(0, idx);
            idx = Math.max(port.lastIndexOf('\\'), port.lastIndexOf('/'));
            port = idx == -1 ? port : port.substring(idx + 1, port.length());
            InputStream inputStream = new LoggerInputStream(connection.openInputStream(), port);
            outputStream = new LoggerOutputStream(connection.openOutputStream());
            eventReader = new RadioMACFrameReader(inputStream, primitivesFactory, this);
            upperLayerNotifier = new SingleThreadExecutor("MAC-upper-layer-notifier");
            eventReader.start();
            if (log.isInfoEnabled()) {
                log.info("started...");
            }
            synchronized (state) {
                state[0] = STATE_STARTED;
            }
        } catch (Exception e) {
            log.error("error starting layer", e);
            freeResources();
            synchronized (state) {
                state[0] = STATE_STOPPED;
            }
            getStack().getEventListener().handle(new FailureStackEvent(ToStringUtil.getSimpleName(e.getClass()),
                    SystemCodes.FAILURE_LAYER_GENERAL, e));
        }
    }

    public void stop() {
        // we run stop in any state (to finally get to STOPPED state)
        synchronized (state) {
            state[0] = STATE_TRANSITION;
        }
        if (log.isInfoEnabled()) {
            log.info("stopping...");
        }
        try {
            freeResources();
            if (log.isDebugEnabled()) {
                log.debug("stopped...");
            }
        } finally {
            synchronized (state) {
                state[0] = STATE_STOPPED;
            }
        }
    }

    private void freeResources() {
        if (upperLayerNotifier != null) {
            upperLayerNotifier.kill();
            upperLayerNotifier = null;
        }
        if (eventReader != null) {
            eventReader.interrupt();
            eventReader = null;
        }
        try {
            if (connection != null) {
                connection.close();
            }
        } catch (IOException e) {
            log.error("error closing connection", e);
        } finally {
            connection = null;
        }
    }

    // --- execution of command (EventDispatcher uses it)

    private void launch(StackEvent se) {
        synchronized (state) {
            if (state[0] != STATE_STARTED) {
                throw new IllegalStateException("layer is not started");
            }
        }
        if (!(se instanceof MACCommand)) {
            throw new IllegalArgumentException("unknown command " + se);
        }
        // in STARTED we always have connection
        /*if (outputStream == null) {
            throw new IllegalStateException("no physical connection");
        }*/
        final MACCommand cmd = (MACCommand) se;
        synchronized (writeBuffer) {
            try {
                if (log.isDebugEnabled()) {
                    log.debug("sending " + cmd);
                }
                writeBuffer.position(0);
                writeBuffer.limit(cmd.getSize(this) + 2); // +2 - size+code
                writeBuffer.put((byte) (cmd.getSize(this) + 1)); // +1 - first byte - frame content code+command
                writeBuffer.put(cmd.getCode());
                cmd.serialize(writeBuffer, this);
                final byte[] bytes = writeBuffer.array();
                synchronized (activeCommands) {
                    outputStream.write(bytes, 0, cmd.getSize(this) + 2); // +2 - size+code
                    outputStream.flush();
                    if (cmd instanceof MACReplyAwaited) {
                        activeCommands.add(cmd);
                        if (log.isDebugEnabled()) {
                            log.debug("command stored for reply matching " + cmd);
                        }
                    }
                }
            } catch (Exception e) {
                // we consider it to be fatal error and stopping layer
                // Note: there can be errors with .writeBytes() that doesn't affect layer itself, but they reflect
                //       bad unstable code
                log.error("error sending command " + cmd, e);
                stop();
                getStack().getEventListener().handle(new FailureStackEvent(ToStringUtil.getSimpleName(e.getClass()),
                        SystemCodes.FAILURE_LAYER_GENERAL, e));
            }
        }
    }

    // --- event from RadioMACFrameReader

    void acceptEvent(final StackEvent event) {
        synchronized (state) {
            if (state[0] != STATE_STARTED) {
                if (log.isDebugEnabled()) {
                    log.debug("layer not started, ignoring " + event);
                }
                return;
            }
        }
        if (event instanceof FailureStackEvent) {
            stop();
            getStack().getEventListener().handle(event);
        } else {
            if (upperLayerNotifier != null) {
                upperLayerNotifier.execute(new Runnable() {
                    public void run() {
                        doMatchAndFireEvent(event);
                    }
                });
            } else if (log.isInfoEnabled()) {
                log.info("upper layer notifier is null, event discarded " + event);
            }
        }
    }

    private void doMatchAndFireEvent(StackEvent event) {
        if (event instanceof MACReplyOnCommand) {
            synchronized (activeCommands) {
                Iterator it = activeCommands.iterator();
                boolean matched = false;
                while (it.hasNext()) {
                    StackEvent command = (StackEvent) it.next();
                    if (((MACReplyOnCommand) event).isReplyOn(command)) {
                        event.setID(command.getID());
                        event.setContext(command.getContext());
                        it.remove();
                        if (log.isDebugEnabled()) {
                            log.debug("reply matching performed, " + event + " isReplyOn " + command);
                        }
                        matched = true;
                        break;
                    }
                }
                if (!matched && log.isDebugEnabled()) {
                    log.debug("reply matching failed for " + event);
                }
            }
        }else{
            log.info("***!!!*** passing \"non-reply on command\" event: " + event);
        }
        upperLayer.getEventListener().handle(event);
    }

    // --- execute 'implementation'

    public synchronized StackConfirm execute(StackRequest cmd) {
        throw new UnsupportedOperationException("execute(StackRequest) is not supported");
    }

    public synchronized StackConfirm execute(StackRequest cmd, long timeout) {
        throw new UnsupportedOperationException("execute(StackRequest, long) is not supported");
    }
}
